﻿#region Disclaimer/Info
///////////////////////////////////////////////////////////////////////////////////////////////////
// MetaCollection
// 
// MetaCollection is a system to provide a standard structure, permissions and metadata service
// for content organization.
//
// This project is hosted at Google Code at http://code.google.com/p/metacollection/
//
// This project is licensed under the BSD license.  See the License.txt file for more information.
///////////////////////////////////////////////////////////////////////////////////////////////////
#endregion

using System;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Threading;
using System.Web;
using System.Xml.Serialization;
using MetaCollection.Framework.Model;
using MetaCollection.Framework.Repository;
using MetaCollection.Framework.Security;
using System.Security.Principal;

namespace MetaCollection.Framework
{
	/// <summary>
	/// A RequestHandler is responsible for the execution of the events in the request pipeline
	/// in the proper order.  The implementation of events is provided by 
	/// <see cref="MetaCollection.Framework.Providers.IProviders"/> which are bound to the RequestHandler
	/// through the Register method.
	/// 
	/// The <see cref="MetaCollection.RequestHandlerFactory"/> takes care of instantiating the needed
	/// RequestHandlers and binding not only the primary IProviders used to handle items with matching
	/// ResourceProviderIds but also any other IProviders or IRequestSinks that may wish to handle
	/// events across all RequestHandler instances maintained by the ItemApplication.
	/// 
	/// The RequestHandler does most of it's work through the ProcessRequest method.  This method handles
	/// the invokation of IRequestSink events in the proper order.  State of a request is maintained through
	/// a CurrentContext <see cref="MetaCollection.ItemContext"/> property shared among IProviders and 
	/// IRequestSinks.
	/// 
	/// Publicly the RequestHandler will be worked with through the methods on the IRequestHandler.
	/// These methods allow querying and updating the collection.
	/// </summary>
	public sealed class RequestHandler : IDisposable, IRequestHandler
	{
		/// <summary>
		/// Internal locking object that locks the event system during changes or reads.
		/// </summary>
		readonly object m_RequestEventLock = new object();
		/// <summary>
		/// The collection of event delegates registered against for the processing of the request.
		/// </summary>
		Dictionary<RequestEventTypes, EventHandler<EventArgs>> m_EventList;
		/// <summary>
		/// A collection of known root items which are used to start processing Item Request URIs
		/// </summary>
		Dictionary<string, IItemInstance> m_RootItems;

		IItemRepository m_ItemRepository;
		IResourceRepository m_ResourceRepository;
		IModelRepository m_ModelRepository;
		IAuditRepository m_AuditRepository;

		/// <summary>
		/// Initializes a new instance of the <see cref="RequestHandler"/> class.
		/// </summary>
		/// <param name="provider">The resource provider that implements the services for this Handler.</param>
		internal RequestHandler(Dictionary<string, IItemInstance> rootItems, IItemRepository itemRepository, IResourceRepository resourceRepository, IModelRepository modelRepository, IAuditRepository auditRepository)
		{
			if (itemRepository == null)
				throw new ArgumentNullException("itemRepository", "Item Repository Can not be Null.");
			if (resourceRepository == null)
				throw new ArgumentNullException("resourceRepository", "Resource Repository Can not be Null.");

			m_RootItems = rootItems;

			m_ItemRepository = itemRepository;
			m_ModelRepository = modelRepository;
			m_ResourceRepository = resourceRepository;
			m_AuditRepository = auditRepository;

			m_EventList = new Dictionary<RequestEventTypes, EventHandler<EventArgs>>();
			foreach (RequestEventTypes eventType in Enum.GetValues(typeof(RequestEventTypes)))
			{
				lock (m_RequestEventLock)
					m_EventList.Add(eventType, null);
			}
		}

		/// <summary>
		/// Registers the specified sink for notifications of the events in eventTypes.
		/// </summary>
		/// <param name="sink">The sink.</param>
		/// <param name="eventTypes">The event types.</param>
		public void Register(IRequestSink sink, RequestEventTypes eventTypes)
		{
			foreach (RequestEventTypes eventType in eventTypes.GetEnumerator())
			{
				lock (m_RequestEventLock)
				{
					m_EventList[eventType] += eventType.GetEventHandler(sink);
				}
			}
		}

		/// <summary>
		/// Unregisters the specified sink of the events in eventTypes.
		/// </summary>
		/// <param name="sink">The sink.</param>
		/// <param name="eventTypes">The event types.</param>
		public void Unregister(IRequestSink sink, RequestEventTypes eventTypes)
		{
			foreach (RequestEventTypes eventType in eventTypes.GetEnumerator())
			{
				lock (m_RequestEventLock)
				{
					m_EventList[eventType] -= eventType.GetEventHandler(sink);
				}
			}
		}
		/// <summary>
		/// Invokes a Resource Request on the collection.
		/// </summary>
		/// <param name="user">The MetaPrincipal that the request should be performed as</param>
		/// <param name="reqeustType">Type of the reqeust.</param>
		/// <param name="itemPath">The item path.</param>
		/// <param name="stream">An optional stream to read an item resource from when saving.</param>
		/// <returns>A stream for reading the ItemResource data in Get type requests, <c>null</c> in Resource
		/// Set type requests</returns>
		public Stream ResourceRequest(MetaPrincipal user, RequestTypes requestType, Uri itemPath, params object[] requestData)
		{
			Stream stream = (Stream)requestData.FirstOrDefault(r => r is Stream, null);
			int? specificVersion = (int?)requestData.FirstOrDefault(r => r is Nullable<int>, null);

			if (itemPath == null)
				throw new ArgumentNullException("itemPath", "The Path to the Requested Item cannot be null or empty.");

			if (requestType.HasType(RequestTypes.GetResource))
			{
				ItemContext context = new ItemContext(user, m_ItemRepository, m_ResourceRepository, m_ModelRepository, m_AuditRepository, requestType, GetRootItem(itemPath), itemPath, specificVersion);
				ProcessRequest(context);
				return context.ResponseResourceStream;
			}
			else if (requestType.HasType(RequestTypes.SetResource))
			{
				if (stream == null || !stream.CanRead)
					throw new ArgumentException("The resource stream can not be null and must support reading.", "requestData");
				ItemContext context = new ItemContext(user, m_ItemRepository, m_ResourceRepository, m_ModelRepository, m_AuditRepository, RequestTypes.SetResource, GetRootItem(itemPath), itemPath, stream);
				ProcessRequest(context);
				return null;
			}
			else
			{
				throw new ArgumentException("Only GetResource and SetResource Resource Types are allowed.");
			}			 
		}

		/// <summary>
		/// Invokes an Item Request on the collection.
		/// </summary>
		/// <param name="user">The MetaPrincipal that the request should be performed as</param>
		/// <param name="requestType">Type of the request.</param>
		/// <param name="itemPath">The item path.</param>
		/// <param name="requestData">For requests that require an Item to be submitted (Set, Query) this parameter should
		/// contain an IItemInstance as the first item in the array.  For PostToItem requests a NameValue Collection should
		/// be passed in as the first parameter. As an optional parameter for all requests a LoadOptions specification can
		/// be provided.</param>
		/// <returns>
		/// For Get Type <paramref name="requestType"/> requests this method will return an enummerable list of items.
		/// In some cases this will be an empty list if no items can be found to match the request or in the
		/// case of a Delete request there is no item to return.
		/// </returns>
		/// <example>
		/// <c>
		/// RequestHandler.ItemRequest(RequestType.GetItem, new Uri("col://root/item.extension"));
		/// RequestHandler.ItemRequest(RequestType.GetItem, new Uri("col://root/item.extension"), LoadOptions.ItemComplete);
		/// RequestHandler.ItemRequest(RequestType.SetItem, new Uri("col://root/item.extension"), updatedItemInstance);
		/// RequestHandler.ItemRequest(RequestType.SetItem, new Uri("col://root/item.extension"), updatedItemInstance, LoadOptions.ItemComplete);
		/// RequestHandler.ItemRequest(RequestType.QueryItem, new Uri("col://root/item.extension"), itemToMatch);
		/// RequestHandler.ItemRequest(RequestType.QueryItem, new Uri("col://root/item.extension"), itemToMatch, LoadOptions.ItemReference);
		/// </c>
		/// </example>
		public IEnumerable<IItemInstance> ItemRequest(MetaPrincipal user, RequestTypes requestType, Uri itemPath, params object[] requestData)
		{
			if (itemPath == null)
				throw new ArgumentNullException("itemPath", "The Path to the Requested Item cannot be null or empty.");

			ItemContext context = new ItemContext(user, m_ItemRepository, m_ResourceRepository, m_ModelRepository, m_AuditRepository, requestType, GetRootItem(itemPath), itemPath, requestData);
			ProcessRequest(context);
			return context.ResponseItems;
		}

		/// <summary>
		/// Returns the Item Specification (Validation Model) associated with the item identified by
		/// the <paramref name="itemPath"/>.
		/// </summary>
		/// <param name="user">The MetaPrincipal that the request should be performed as</param>
		/// <param name="itemPath">the path of the item in the collection to retrieve the specifications for</param>
		/// <returns>
		/// The Item Model Specification.
		/// </returns>
		public IItemSpecification ItemSpecificationRequest(MetaPrincipal user, Uri itemPath)
		{
			if (m_ModelRepository == null)
				return null;

			ItemContext context = new ItemContext(user, m_ItemRepository, m_ResourceRepository, m_ModelRepository, m_AuditRepository, RequestTypes.GetItemSpecification, GetRootItem(itemPath), itemPath);
			ProcessRequest(context);
			return context.ItemSpecification;
		}

		/// <summary>
		/// Gets the root item for a given Item Path URI
		/// </summary>
		/// <param name="itemPath">The item path.</param>
		/// <returns>The root item registered for this URI (a registered item with a name that matches the HostName of the URI)</returns>
		private IItemInstance GetRootItem(Uri itemPath)
		{
			KeyValuePair<string, IItemInstance> itemRecord = m_RootItems.FirstOrDefault((r) => r.Key.Equals(itemPath.Host, StringComparison.OrdinalIgnoreCase));
			if (string.IsNullOrEmpty(itemRecord.Key))
			{
				throw new ArgumentException("Specified Root Item is not Registered.", "itemPath");
			}
			return itemRecord.Value;
		}
		
		/// <summary>
		/// Raises the specified event types.
		/// </summary>
		/// <param name="eventTypes">The event types.</param>
		[System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Design", "CA1031:DoNotCatchGeneralExceptionTypes", Justification = "Even though we are not throwing the general exception caught in our worker delegate we are going to rethrow it once out of our worker thread.")]
		private void Raise(ItemContext context, RequestEventTypes eventTypes)
		{
#if !SYNC
			List<WaitHandle> waitHandles = new List<WaitHandle>();
#endif
			foreach (RequestEventTypes eventType in eventTypes.GetEnumerator())
			{
				EventHandler<EventArgs> handler;
				lock (m_RequestEventLock)
				{
					handler = m_EventList[eventType];
				}
				if (handler != null)
				{
#if SYNC
					foreach (EventHandler<EventArgs> e in handler.GetInvocationList())
					{
						e(context, EventArgs.Empty);
					}
#else
					foreach (EventHandler<EventArgs> e in handler.GetInvocationList())
					{
						var ev = e; // NOTE: required so closure binds to correct event
						ManualResetEvent mrEvent = new ManualResetEvent(false);
						ThreadPool.QueueUserWorkItem(delegate
						{
							try
							{
								ev(context, EventArgs.Empty);
							}
							catch (Exception ex)
							{
								lock (m_RequestEventLock)
								{
									// TODO : perhaps we don't want to blow away the exceptions are already there?
									if (context != null)
										context.ExecutionException = ex;
									foreach (ManualResetEvent wHandles in waitHandles)
										wHandles.Set();
								}
							}
							finally
							{
								mrEvent.Set();
							}
						});

						waitHandles.Add(mrEvent);
					}
					ThreadSafeWait(waitHandles);
					lock (m_RequestEventLock)
					{
						if (context != null && context.ExecutionException != null)
							throw context.ExecutionException;
					}
#endif
				}
			}
		}
#if !SYNC
		/// <summary>
		/// Blocks the current thread for all the WaitHandles given
		/// </summary>
		/// <remarks>
		/// This method provides a Thread Apartment state neutral method for waiting for all
		/// the thread pool entries to complete.  There is no timeout given.
		/// </remarks>
		/// <param name="waitHandles">handles to block for</param>
		private static void ThreadSafeWait(List<WaitHandle> waitHandles)
		{
			if (Thread.CurrentThread.GetApartmentState() == ApartmentState.MTA)
			{
				WaitHandle.WaitAll(waitHandles.ToArray());
			}
			else if (Thread.CurrentThread.GetApartmentState() == ApartmentState.STA)
			{
				ManualResetEvent mrEvent = new ManualResetEvent(false);
				ThreadPool.QueueUserWorkItem(delegate
				{
					WaitHandle.WaitAll(waitHandles.ToArray());
					mrEvent.Set();
				});
				mrEvent.WaitOne();
			}
			else
			{
				throw new NotSupportedException(String.Format(CultureInfo.CurrentUICulture, "Apartment State of {0} is not supported", Thread.CurrentThread.GetApartmentState()));
			}
		}
#endif
		/// <summary>
		/// Processes the request.
		/// </summary>
		/// <param name="context">The item request context.</param>
		/// <returns>[true] if process completed.</returns>
		public bool ProcessRequest(ItemContext context)
		{
			if (context == null)
				throw new ArgumentNullException("context", "Item Context for the request can not be null.");

			Raise(context, RequestEventTypes.OnRequestStart | RequestEventTypes.OnBeforePathResolve);

			while (!context.RequestPathResolved.GetValueOrDefault(false))
			{
				Raise(context, RequestEventTypes.OnPathResolve);

				if (!context.RequestPathResolved.HasValue)
					throw new InvalidOperationException("Call for path resolution was made but no action was taken.");

				// if the child does not exist then return false so that the request can be aborted.
				if (context.RequestedItem == null)
					return false;
			}

			Raise(context, RequestEventTypes.OnPathResolveComplete);

			if (context.RequestType.HasAnyType(RequestTypes.PostToItem | RequestTypes.SetItem | RequestTypes.SetResource))
			{
				Raise(context, RequestEventTypes.OnBeforeValidateRequest | RequestEventTypes.OnValidateRequest);
				if (!context.IsValidRequest.GetValueOrDefault(false))
					throw new InvalidOperationException("Validation of the Item Request Failed or was not completed.", context.ExecutionException);
				Raise(context, RequestEventTypes.OnRequestValidateComplete);
			}

			Raise(context, RequestEventTypes.OnBeforeInvokeRequest);

			if (context.RequestType.HasAnyType(RequestTypes.GetItem | RequestTypes.SetItem | RequestTypes.DeleteItem | RequestTypes.GetItemSpecification))
				Raise(context, RequestEventTypes.OnProvideItem);

			if (context.RequestType.HasAnyType(RequestTypes.GetResource | RequestTypes.SetResource))
				Raise(context, RequestEventTypes.OnProvideResource);

			Raise(context, RequestEventTypes.OnRequestComplete);

			return true;
		}

		#region IDisposable Members

		/// <summary>
		/// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources.
		/// </summary>
		public void Dispose()
		{
			Raise(null, RequestEventTypes.OnHandlerDisposing);
			this.m_EventList[RequestEventTypes.OnHandlerDisposing] = null;
		}

		#endregion

	}

}
